﻿###############################
# Snapraid Sync Helper Script #
###############################
# this is a helper script that keeps snapraid parity info in sync with
# your data. 
# See the codeplex page for documentation and functionallity: http://snapraidhelperwin.codeplex.com/
#
# $Author: lrissman at gmail dot com
#
# Originally based on bash script written by sidney for linux/bash
# 
#######################################################################

<#

Note:  To run a powershell script you must perform the following:
Only Once:
1) Download the PowerShellExecutionPolicy.adm from http://go.microsoft.com/fwlink/?LinkId=131786.
2) Install it
3) open gpedit.msc
4) Under computer configuration, right-click Administrative Templates and then click Add/Remove Templates
5) Add PowerShellExecutionPolicy.adm from %programfiles%\Microsoft Group policy
6) Open Administrative Templates\Classic Administrative Templates\Windows Components\Windows PowerShell
7) Enable the property and allow unsigned scripts to be run

each time:
1) open the power shell prompt
2) from the directory of video files, run the script
#>

##########################################
############# NOTES ######################
##########################################
<#   
- This script depends upon the powershell community extensions from http://pscx.codeplex.com/
   - Or direct sownload: http://pscx.codeplex.com/downloads/get/523236
- Snapraid's output is unix formatted (CRLF vs CR) so -delim "`0" is required to have each line on newline when using Get-Content
- Service Start and Stop requires the script to run with Elevated Rights
- snapraid-helper.ini file is required in the same dir as the .ps1
 #>
##########################################
############# END NOTES ##################
##########################################
 
##############################################
############# FUNCTIONS ######################
##############################################

 function Test-IsAdmin {     #Borrowed from with some modifications: http://stackoverflow.com/questions/9999963/powershell-test-admin-rights-within-powershell-script
    try {
        $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = New-Object Security.Principal.WindowsPrincipal -ArgumentList $identity
        return $principal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator )
    } catch {
        #throw "Failed to determine if the current user has elevated privileges. The error was: '{0}'." -f $_
        return 0
    }
    return 1
}

# Build Email Function (used many times in script)
Function Send-Email ($fSubject,$fSuccess,$EmailBody){
    #$fSubject -- passed subject line
    #$fSuccess -- "success" = success email, "error" = error emai
    $Body = ""
    
    if (($fSuccess -eq "success") -and ($config["EmailOnSuccess"] = 1) -and ($config["EmailEnable"] = "TRUE")) {
        if ($IncludeExtendedInfo = 1 ){
            #$Body = Get-Content $EmailBody -delim "`0" 
            #Get-Content is VERY VERY VERY Slow
            $RawBody = [System.IO.File]::ReadAllLines($EmailBody)
            # Below adds a "Newline"  char to each line
            foreach ($line in $RawBody){
             $Body = $body + "$line`n"
            }
           
        }
        $SMTPClient.Send($config["EmailFrom"],$config["EmailTo"],$fSubject,$Body)
    }
    if (($fSuccess -eq "error") -and ($config["EmailOnError"] = 1) -and ($config["EmailEnable"] = "TRUE")) {
        if ($config["IncludeExtendedInfo"] = 1 ){
           #$Body = Get-Content $config["EmailBody"] -delim "`0"
           #Get-Content is VERY VERY VERY Slow
           $RawBody = [System.IO.File]::ReadAllLines($EmailBody)
           foreach ($line in $RawBody){
             $Body = $body + "$line`n"
           }
        }
        $SMTPClient.Send($config["EmailFrom"],$config["EmailTo"],$fSubject,$Body)
    }

}

##############################################
############### END FUNCTIONS ################
##############################################

 #Lets do some base variables, and ensure some values are empty
$HomePath = $MyInvocation.Line | Split-Path
$message = ""
$ConfigError = 0

##############################################
###### Configuration Verification Start ######
##############################################
 # Get variables from snapraid-helper.ini
 $ConfigFile="$HomePath\snapraid-helper.ini"
 $config = @{}
 
 Get-Content $ConfigFile | foreach {
 	if (($_.StartsWith(";")) -or (!($_))) {
		#Non-variable or is Space
    #    write-host "Non-Variable: $_"
	}
    else {
        $line = $_.Split("=")
	    #$config.($line[0]) = $line[1]
        $config[$line[0]] = $line[1].TrimEnd()
     #   write-host Variable: $line[0]  Content: $line[1]
    }
}
##### Validate configuration variables are sane

#SnapRAID and LogFile Config
$SnapRAIDConfigs = "SnapRAIDDelThreshold","SnapRAIDPath","SnapRAIDExe","SnapRAIDContentFile","SnapRAIDParityFile","TmpOutputFile","LogFileName","LogFileMaxSize"
foreach ($element in $SnapRAIDConfigs){
    if (!($config[$element]) -or ($config[$element] -eq "")) {
        write-host "$element is null, please add a value"
        $ConfigError ++
    }
}

#Validate EmailBodyPath and if not specified, use ScriptPath
if (!($config["LogPath"]) -or ($config["LogPath"] -eq "") ) { 
    $config["LogPath"] = "$HomePath\" 
}

if ( !(Test-Path $config["LogPath"] -pathType container) ) {
    Write-host "ERROR: LogPath: "$config["LogPath"]"  - Path Does not exist.  Please fix $ConfigFile or create the path"
    exit 1
}

$LogFile=$config["LogPath"] + $config["LogFileName"]

# Start Transcript logging
# redirect all stdout to log file (leave stderr alone thou)
$ErrorActionPreference="SilentlyContinue"
Stop-Transcript | out-null
$ErrorActionPreference = "Continue"
Start-Transcript -path $LogFile -append

#Email Configs
$EmailConfigs = "SubjectPrefix","EmailTo","EmailFrom","Body","SMTPServer","SMTPHost","SMTPSSLEnable","EmailBodyFile","EmailEnable","SMTPPort","SMTPUID","SMTPPass","EmailOnSuccess","EmailOnError","IncludeExtendedInfo"
#If email is enabled, validate email configs are not null
if ($config["EmailEnable"] -eq 1){
    foreach ($element in $EmailConfigs){
        if (!($config[$element]) -or ($config[$element] -eq "")) {
            write-host "$element is null, please add a value"
            $ConfigError ++
        }
        
    }
}

#Service/Process Configs
if ($config["ServiceEnable"] -eq 1){ 
    if (!(Test-IsAdmin)) {
        Write-Host "You need to run the script with elevated rights to start and stop services. Either run with Elevated Rights or change in $ConfigFile ProcessEnable=0"
        exit 1
    }
    $ServiceConfigs = "ServiceName"
    #If process handling is enabled, validate configs are not null
    if ($config["ServiceEnable"] -eq 1){
        foreach ($element in $ServiceConfigs){
            if (!($config[$element]) -or ($config[$element] -eq "")) {
                write-host "$element is null, please add a value"
                $ConfigError ++
            }
            
        }
    }
    $ServiceNum = 0
    $ServiceList = $config["ServiceName"].Split(",").Replace('"',"")
    foreach ($Service in $ServiceList) {
        $ServiceNum ++
        if (!(Get-Service $Service -ErrorAction SilentlyContinue))
        {
            "The Service $Service does not exist.   Please remove or correct in ProcessName in $ConfigFile"
        }
    }
}

#EventLog Configs
$EventLogConfigs = "EventLogSources","EventLogEntryType","EventLogDays","EventLogHaltOnDiskError"
if ($config["EventLogEnable"] -eq 1){
    foreach ($element in $EventLogConfigs){
        if (!($config[$element]) -or ($config[$element] -eq "")) {
            write-host "$element is null, please add a value"
            $ConfigError ++
        }
        
    }
    $EventLogEntryTypeList = $config["EventLogEntryType"].Split(",").Replace('"',"").Trim()
    $EventLogSourcesList = $config["EventLogSources"].Split(",").Replace('"',"").Trim()
}


#Report if there are errors and exit
if ($ConfigError -ge 1) {
    write-host "Number of config errors: $ConfigError"
    write-host "Please correct $ConfigFile and run again"
    exit 1
 }

#Validate EmailBodyPath and if not specified, use Windows Temp path
if (!($config["EmailBodyPath"]) -or ($config["EmailBodyPath"] -eq "") ) { 
    $config["EmailBodyPath"] = "$env:temp\" 
}

if ( !(Test-Path $config["EmailBodyPath"] -pathType container) ) {
    Write-host "ERROR: EmailBodyPath: "$config["EmailBodyPath"]"  - Path Does not exist.  Please fix $ConfigFile or create the path"
    exit 1
}


#Validate TmpOutputPath and if not specified, use Windows Temp path
if (!($config["TmpOutputPath"]) -or ($config["TmpOutputPath"] -eq "")){ 
    $config["TmpOutputPath"] = "$env:temp\" 
}

if ( !(Test-Path $config["TmpOutputPath"] -pathType container) ) {
    Write-host "ERROR: TmpOutputPath:" $config["TmpOutputPath"]"  - Path Does not exist.  Please fix $ConfigFile or create the path"
    exit 1
}

##############################################
###### Configuration Verification End ########
##############################################

#Initalize Email
if ($config["EmailEnable"] -eq 1) {
    $SMTPClient = New-Object Net.Mail.SmtpClient
    $SMTPServer = $config["SMTPServer"]
    $SMTPClient.Host = $config["SMTPHost"]
    $SMTPClient.Port = $config["SMTPPort"]
    if ($config["SMTPSSLEnable"] -eq 1) {
        $SMTPClient.EnableSsl = $true
    }
    else {
        $SMTPClient.EnableSsl = $false
    }
    $SMTPClient.Credentials = new-Object System.Net.NetworkCredential($config["SMTPUID"],$config["SMTPPass"]); 
}
$TmpOutput=$config["TmpOutputPath"] + $config["TmpOutputfile"]
$EmailBody=$config["EmailBodyPath"] + $config["EmailBodyfile"]

#Start with some cleanup
if (Test-Path $TmpOutput){
	Remove-Item $TmpOutput
}
if (Test-Path $EmailBody){
	Remove-Item $EmailBody
}

#Log Management Section
if (Test-Path "$LogFile") { 
    $file = Get-Item "$LogFile"
    $filesize = $file.length / 1KB
}
else {
    $filesize = 0
}


if ($filesize -ge $config["LogFileMaxSize"]) {
	#Pause trancoding to rotate logs
	$ErrorActionPreference="SilentlyContinue"
	Stop-Transcript | out-null
    if (Test-Path "$LogFile.5.zip") {
        Remove-Item "$LogFile.5.zip"
    }
    if (Test-Path "$LogFile.4.zip") {
        Rename-Item "$LogFile.4.zip" "$LogFile.5.zip"
    }
    if (Test-Path "$LogFile.3.zip") {
        Rename-Item "$LogFile.3.zip" "$LogFile.4.zip"
    }
    if (Test-Path "$LogFile.2.zip") {
        Rename-Item "$LogFile.2.zip" "$LogFile.3.zip"
    }
    if (Test-Path "$LogFile.1.zip") {
        Rename-Item "$LogFile.1.zip" "$LogFile.2.zip"
    }
    Write-zip "$LogFile" -level 9
    Rename-Item "$LogFile.zip" "$LogFile.1.zip"
    Remove-Item "$LogFile"
    New-Item "$LogFile" -type file
	#Restart transcoding
	$ErrorActionPreference = "Continue"
	Start-Transcript -path $LogFile -append

}
#Check Eventlog for Errors
$CurrentDate = Get-Date
$message = "Checking for Disk issues in Eventlog at $CurrentDate"
Write-Host "----------------------------------------"
Write-Host $message
Write-Host "----------------------------------------"
Add-Content $EmailBody "----------------------------------------"
Add-Content $EmailBody $message
Add-Content $EmailBody "----------------------------------------"

$EventLogOutput = get-eventlog -logname system -entrytype $EventLogEntryTypeList -Source $EventLogSourcesList -After (Get-Date).AddDays($config["EventLogdays"])
Write-Host "TimeGenerated,EntryType,Source,Message"
foreach ($event in $EventLogOutput) {
    $EventLogCount = $EventLogcount + 1
    $TimeGenerated = $event.TimeGenerated
    $EntryType = $event.EntryType
    $Source = $event.Source
    $EventMessage = $event.Message
    
    Write-Host "$TimeGenerated,$EntryType,$Source,$EventMessage"  
    Add-Content $EmailBody "$TimeGenerated,$EntryType,$Source,$EventMessage"  
}

if (($EventLogCount -gt 0) -and ($config["EventLogHaltOnDiskError"] -eq 1)) {
    $message = "WARN:  Found disk Errors/Warnings in EventLogs.  Aborting sync based on HaltOnDiskError"
    Write-Host $message -ForegroundColor red -backgroundcolor yellow
    Add-Content $EmailBody $message
    $subject = $config["SubjectPrefix"]+" "+$message
    Send-Email $subject "error" $EmailBody
    Stop-Transcript | out-null
    exit 1
}

# timestamp the job
$CurrentDate = Get-Date
$message = "SnapRAID DIFF Job started on $CurrentDate"
Write-Host "----------------------------------------"
Write-Host $message
Write-Host "----------------------------------------"
Add-Content $EmailBody "----------------------------------------"
Add-Content $EmailBody $message
Add-Content $EmailBody "----------------------------------------"


#sanity check first to make sure we can access the content and parity files
if (!(Test-Path $config["SnapRAIDContentFile"])){
  $message = "ERROR - Content file (" + $config["SnapRAIDContentFile"] + ") not found!"
  Write-Host $message -ForegroundColor red -backgroundcolor yellow
  Add-Content $EmailBody $message
  $subject = $config["SubjectPrefix"]+$message
  Send-Email $subject "error" $EmailBody
  Stop-Transcript | out-null
  exit 1
}

if (!(Test-Path $config["SnapRAIDParityFile"])){
  $message = "ERROR - Content file ("+ $config["SnapRAIDParityFile"] +") not found!"
  Write-Host $message -ForegroundColor red -backgroundcolor yellow
  Add-Content $EmailBody $message
  $subject = $config["SubjectPrefix"] + $message
  Send-Email $subject "error" $EmailBody
  Stop-Transcript | out-null
  exit 1
}

# run the snapraid DIFF command
$argument = "diff"
$exe = $config["SnapRAIDPath"] + $config["SnapRAIDExe"]
& "$exe" $argument  >> $TmpOutput
#$TmpOutputInRAM = get-content $TmpOutput  -readcount 100 -delim "`0" 
# NOTE the above Get-Content command is VERY VERY VERY VERY slow, so I am using the .Net function below to get the output of the Snapraid command into a variable
$TmpOutputInRAM = [System.IO.File]::ReadAllLines($TmpOutput)
foreach ($line in $TmpOutputInRAM){
    Add-Content $EmailBody $line
    Write-Host $line
}
if (!($LastExistCode = "0")) {
    $CurrentDate = Get-Date
    $message = "ERROR: SnapRAID DIFF Job FAILED on $CurrentDate with exit code $LastExitCode"
    Write-Host "----------------------------------------"
    Write-Host $message
    Write-Host "----------------------------------------"
    Add-Content $EmailBody "----------------------------------------"
    Add-Content $EmailBody $message
    Add-Content $EmailBody "----------------------------------------"
    $subject = $config["SubjectPrefix"] + $message
    Send-Email $subject "error" $EmailBody
    Stop-Transcript | out-null
    exit 1
              
}
# DIFF was successful, move onto processing.
$CurrentDate = Get-Date
$message = "SnapRAID DIFF Job finished on $CurrentDate"
Write-Host "----------------------------------------"
Write-Host $message
Write-Host "----------------------------------------"
Add-Content $EmailBody "----------------------------------------"
Add-Content $EmailBody $message
Add-Content $EmailBody "----------------------------------------"
$DEL_COUNT = $TmpOutputInRAM | Select-String -Pattern "Remove" | Measure-Object -Line
$DEL_COUNT = $DEL_COUNT.Lines
$ADD_COUNT = $TmpOutputInRAM | Select-String -Pattern "Add" | Measure-Object -Line
$ADD_COUNT = $ADD_COUNT.Lines
$MOVE_COUNT = $TmpOutputInRAM | Select-String -Pattern "Move" | Measure-Object -Line
$MOVE_COUNT = $MOVE_COUNT.Lines
$UPDATE_COUNT = $TmpOutputInRAM | Select-String -Pattern "Update" | Measure-Object -Line
$UPDATE_COUNT = $UPDATE_COUNT.Lines

$message = "SUMMARY of changes - Added [$ADD_COUNT] - Deleted [$DEL_COUNT] - Moved [$MOVE_COUNT] - Updated [$UPDATE_COUNT]"
Write-Host $message
Write-Host "----------------------------------------"
Add-Content $EmailBody $message
Add-Content $EmailBody "----------------------------------------"

# check if files have changed
if ( $DEL_COUNT -gt 0 -or $ADD_COUNT -gt 0 -or $MOVE_COUNT -gt 0 -or $UPDATE_COUNT -gt 0 ) {
    # YES, check if number of deleted files exceed DEL_THRESHOLD
    if ( $DEL_COUNT -gt $config["SnapRAIDDelThreshold"] ) {
        # YES, lets inform user and not proceed with the sync just in case
        $message = "Number of deleted files ($DEL_COUNT) exceeded threshold (" + $config["SnapRAIDDelThreshold"] + "). NOT proceeding with sync job. Please run sync manually if this is not an error condition."
        Write-Host $message
        Add-Content $EmailBody $message
        $subject = "$SubjectPrefix $message"
        Send-Email $subject "error" $EmailBody
        Stop-Transcript | out-null
        exit 1
    }
    else {
        # NO, delete threshold not reached, lets run the sync job
        $message = "Deleted files ($DEL_COUNT) did not exceed threshold (" + $config["SnapRAIDDelThreshold"] + "), proceeding with sync job."
        Write-Host $message
        Add-Content $EmailBody $message
		$CurrentDate = Get-Date
        $message = "$CurrentDate Changes detected [A-$ADD_COUNT,D-$DEL_COUNT,M-$MOVE_COUNT,U-$UPDATE_COUNT] and deleted files ($DEL_COUNT) is below threshold (" + $config["SnapRAIDDelThreshold"] + "). Running SYNC Command."
        Write-Host $message
        Add-Content $EmailBody $message

        # If Service Management is enabled, then take services offline
        if ($config["ServiceEnable"] -eq 1) {
                    $message = "Stopping Processes $CurrentDate"
                    Write-Host $message
                    Add-Content $EmailBody $message
                    Write-Host "----------------------------------------"
                    Add-Content $EmailBody "----------------------------------------"
                    foreach ($service in $ServiceList) {
                        $message = Stop-Service $service
                        Add-Content $EmailBody $message
                        Write-Host "----------------------------------------"
                        Add-Content $EmailBody "----------------------------------------"
                    }
                    $message = "Done Stopping Processes $CurrentDate"
                    Write-Host $message
                    Add-Content $EmailBody $message
                    Write-Host "----------------------------------------"
                    Add-Content $EmailBody "----------------------------------------"

        }
        $message = "SnapRAID SYNC Job started on $CurrentDate"
        Write-Host $message
        Add-Content $EmailBody $message
        Write-Host "----------------------------------------"
        Add-Content $EmailBody "----------------------------------------"
		$argument = "sync"
        $exe = $config["SnapRAIDPath"] + $config["SnapRAIDExe"]
        & "$exe" $argument >> $TmpOutput

        # If Service Management is enabled, then bring services back online
        if ($config["ServiceEnable"] -eq 1) {
                    $message = "Stopping Processes $CurrentDate"
                    Write-Host $message
                    Add-Content $EmailBody $message
                    Write-Host "----------------------------------------"
                    Add-Content $EmailBody "----------------------------------------"
                    foreach ($service in $ServiceList) {
                        $message = Start-Service $service
                        Add-Content $EmailBody $message
                        Write-Host "----------------------------------------"
                        Add-Content $EmailBody "----------------------------------------"
                    }
                    $message = "Done Stopping Processes $CurrentDate"
                    Write-Host $message
                    Add-Content $EmailBody $message
                    Write-Host "----------------------------------------"
                    Add-Content $EmailBody "----------------------------------------"

        }

        # get-content $TmpOutput  -delim "`0" | Add-Content $EmailBody
        # NOTE the above Get-Content command is VERY VERY VERY VERY slow, so I am using the .Net function below to get the output of the Snapraid command into a variable
        $TmpOutputInRAM = [System.IO.File]::ReadAllLines($TmpOutput)
        foreach ($line in $TmpOutputInRAM){
            Add-Content $EmailBody $line
            Write-Host $line
        }
		if ($LastExistCode = "0" ) {
        	Write-Host "----------------------------------------"
            Add-Content $EmailBody "----------------------------------------"
			$CurrentDate = Get-Date
            $message = "SnapRAID SYNC Job finished on $CurrentDate"
        	Write-Host $message
            Add-Content $EmailBody $message
      	    $subject = "$SubjectPrefix $message"
	    	Send-Email $subject "success" $EmailBody
		}
		else
		 {
        	Write-Host "----------------------------------------"
            Add-Content $EmailBody "----------------------------------------"
			$CurrentDate = Get-Date
            $message = "ERROR SnapRAID SYNC Job FAILED on $CurrentDate"
        	Write-Host $message
            Add-Content $EmailBody $message
      	    $subject = "$SubjectPrefix $message"
    	    Send-Email $subject "error" $EmailBody
		}
		
    }
}
else {
    # NO, so lets log it and exit
	$CurrentDate = Get-Date
    Write-Host "$CurrentDate No change detected. Nothing to do"
}
# End Transcript
Stop-Transcript | out-null
